<?php

namespace app\service;

use app\model\WechatLogDao;
use DOMDocument;
use RuntimeException;
use think\facade\Request;

/**
 * 微信第三方平台
 */
class WechatComponent
{

    private function send(string $method, string $url, string $data): array
    {
        $start = microtime(true);
        $log = new WechatLogDao();
        $log->save([
            'url' => $url,
            'request' => $data,
            'ip' => Request::ip(),
            'uid' => request()->uid,
        ]);
        if (strtoupper($method) === 'POST') {
            $ret = curl_post($url, $data);
        } else {
            $ret = curl_get($url);
        }
        $time = (microtime(true) - $start) * 1000;
        $log->save([
            'response' => $ret,
            'time' => $time,
        ]);
        return [$ret, $log->id];
    }

    /**
     * 第四部：生成授权链接
     * @link https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html#_1%E3%80%81%E6%8E%88%E6%9D%83%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
     */
    public function getAuthUrl()
    {
        $preAuthCode = $this->getPreAuthCode();
        $componentAppid = config('wechat.component_appid');
        $url = config('wechat.notify_host') . '/wechat/authorize?appid=' . config('wechat.appid');
        $url = urlencode($url);
        return 'https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=' . $componentAppid . '&pre_auth_code=' . $preAuthCode . '&redirect_uri='.$url.'&auth_type=3';
    }

    //https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/creat_token.html

    /**
     * 第三步：获取预授权码
     * @link https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/ticket-token/getPreAuthCode.html
     * @return void
     */
    public function getPreAuthCode()
    {
        $key = 'pre_auth_code';
        $preAuthCode = cache($key);
        if (!empty($preAuthCode)) {
            return $preAuthCode;
        }
        $preAuthCode = $this->preAuthCode();
        cache($key, $preAuthCode, 7000);
        return $preAuthCode;
    }


    private function preAuthCode()
    {
        $accessToken = $this->getAccessToken();
        if (!$accessToken) {
            show_error('Invalid component_access_token');
        }
        $url = 'https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?access_token=' . $accessToken;
        $postData = [
            'component_appid' => config('wechat.component_appid'),
        ];
        list($ret, $logId) = $this->send('POST', $url, json_encode($postData));
        if (empty($ret)) {
            show_error('Invalid response');
        }
        $ret = json_decode($ret, true);
        $preAuthCode = $ret['pre_auth_code'] ?? '';
        if (empty($preAuthCode)) {
            $msg = $ret['errmsg'] ?? 'Invalid response';
            WechatLogDao::where('id', $logId)->save([
                'code' => $ret['errcode'] ?? '500',
                'message' => $msg,
            ]);
            show_error($msg);
        }
        WechatLogDao::where('id', $logId)->save([
            'code' => 'OK',
            'message' => 'SUCCESS',
        ]);
        return $preAuthCode;
    }


    /**
     * 第二步：获取令牌
     * @link https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/ticket-token/getComponentAccessToken.html
     *
     */
    public function getAccessToken()
    {
        $key = 'component_access_token';
        $accessToken = cache('component_access_token');
        if (!empty($accessToken)) {
            return $accessToken;
        }
        $accessToken = $this->accessToken();
        cache($key, $accessToken, 7000);
        return $accessToken;
    }


    /**
     * 令牌
     * https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/component_access_token.html
     * @return void
     */
    private function accessToken()
    {
        $ticket = cache('wechat_ticket');
        if (!$ticket) {
            show_error('Invalid ticket');
        }
        $url = 'https://api.weixin.qq.com/cgi-bin/component/api_component_token';
        $postData = [
            'component_appid' => config('wechat.component_appid'),
            'component_appsecret' => config('wechat.component_secret'),
            'component_verify_ticket' => $ticket,
        ];
        list($ret, $logId) = $this->send('POST', $url, json_encode($postData));
        if (empty($ret)) {
            show_error('Invalid response');
        }
        //WechatLogDao::where('id',$logId)
        $ret = json_decode($ret, true);
        $access_token = $ret['component_access_token'] ?? '';
        if (empty($access_token)) {
            $msg = $ret['errmsg'] ?? 'Invalid response';
            WechatLogDao::where('id', $logId)->save([
                'code' => $ret['errcode'] ?? '500',
                'message' => $msg,
            ]);
            show_error($msg);
        }
        WechatLogDao::where('id', $logId)->save([
            'code' => 'OK',
            'message' => 'SUCCESS',
        ]);
        return $access_token;
    }


    /**
     * 第一步：获取Ticket
     * 验证Ticket
     * https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/component_verify_ticket.html
     * @param array $param
     * @param string $xmlRaw
     * @return void
     */
    public function ticket(array $param, string $xmlRaw)
    {
        $xml = new \DOMDocument();
        $xml->loadXML($xmlRaw);
        $appId = trim($xml->getElementsByTagName('AppId')->item(0)->nodeValue);
        $encrypted = trim($xml->getElementsByTagName('Encrypt')->item(0)->nodeValue);
        $timestamp = $param['timestamp'];
        $nonce = $param['nonce'];
        $msgSignature = $param['msg_signature'];
        try {
            $decrypted = $this->decrypt($encrypted, $timestamp, $nonce, $msgSignature, $appId);
            $xml->loadXML($decrypted);
            $ticket = $xml->getElementsByTagName('ComponentVerifyTicket')->item(0)->nodeValue;
            if (!empty($ticket)) {
                cache('wechat_ticket', $ticket, 3600 * 10);
            }
            //dump($ticket);
        } catch (\Exception $e) {
            trace_file($e);
            show_error($e->getMessage(), 500);
        }
    }

    public function message(array $param, string $xmlRaw)
    {

    }


    /**
     * 服务商消息回调 签名验证
     * @link https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/message_push.html
     * @param string $signature
     * @param array $data
     * @return bool
     */
    private function checkSign(string $signature, array $data): bool
    {
        sort($data, SORT_STRING);
        $sign = sha1(implode('', $data));
        return hash_equals($sign, $signature);
    }

    /**
     * 移除PKCS7填充
     * PKCS#7：
     *     K为秘钥字节数（采用 32），
     *     Buf为待加密的内容，其字节数为N
     * Buf需要被填充为K的整数倍。
     *    在 Buf的尾部填充(K-N%K)个字节，每个字节的内容是(K-N%K)
     * @param string $text
     * @return string
     */
    private function pkcs7Unpad(string $text): string
    {
        $blockSize = 32;
        $pad = ord(substr($text, -1));
        if ($pad < 1 || $pad > $blockSize) {
            return $text;
        }
        $padding = substr($text, -1 * $pad);
        if ($padding !== str_repeat(chr($pad), $pad)) {//填充字节校验
            return $text;
        }
        return substr($text, 0, -1 * $pad);
    }

    /**
     * FullStr=random(16B) + msg_len(4B) + msg + appid
     * @param string $msgSignature
     * @param string $timestamp
     * @param string $nonce
     * @param string $encrypt
     * @return string
     */
    public function decrypt(string $encrypt, string $timestamp, string $nonce, string $msgSignature, string $appId): string
    {
        $encodingAesKey = config('wechat.component_key');
        $token = config('wechat.component_token');
        if (!$this->checkSign($msgSignature, [$token, $timestamp, $nonce, $encrypt])) {
            //throw new RuntimeException('Invalid message signature.');
            show_error('Invalid message signature');
        }
        $aesKey = base64_decode($encodingAesKey . '=', true);
        if ($aesKey === false || strlen($aesKey) !== 32) {
            throw new RuntimeException('Invalid AES key');
        }
        $iv = substr($aesKey, 0, 16);
        $cipherText = base64_decode($encrypt, true);
        if ($cipherText === false) {
            throw new RuntimeException('Encrypt field is not valid base64.');
        }
        //FullStr=random(16B) + msg_len(4B) + msg + appid + padding
        $decrypted = openssl_decrypt($cipherText, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
        if ($decrypted === false) {
            throw new RuntimeException('openssl_decrypt failed.');
        }
        $decrypted = $this->pkcs7Unpad($decrypted);//移除PKCS7填充
        $content = substr($decrypted, 16);//移除随机字符串
        $xmlLengthData = substr($content, 0, 4);//读取xml长度
        if (strlen($xmlLengthData) !== 4) {
            throw new RuntimeException('Invalid decrypted message length field.');
        }
        $xmlLength = unpack('N', $xmlLengthData)[1];//读取xml长度：字节码转整形
        $xml = substr($content, 4, $xmlLength);
        $fromAppId = substr($content, 4 + $xmlLength);
        if ($appId !== '' && $fromAppId !== $appId) {
            throw new RuntimeException(sprintf('AppId mismatch: expected %s but got %s', $appId, $fromAppId));
        }
        return $xml;
    }

}